学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Random Lessons from the SwiftUI Digital Lounge . Part 2 [译]

并发

refreshable 是否是 SwiftUI 中唯一支持异步代码的属性呢?

task modifier 也提供了开箱即用的异步代码支持。通常来说,我们只有在能为开发者提供附加的实用工具时才会将你们提供的闭包放进异步环境中执行,比如在 task 附着的视图生命周期结束时取消 task,或者结束刷新动画。当然,如果你希望在别的地方也能把代码分发到异步环境中执行,可以使用 async 代码块!

Swift并发可以向后发布吗?

目前为止你可以认为 Swift 并发无法向后发布,不过开发团队理解这是一个很受欢迎的需求,正在评审。

有趣的部分在于 Swift 是开源的,你可以访问 https://forums.swift.org 来查看 Swift 并发的开发进度,他们会对这个话题进行讨论。

数据管理

@StateObjectObservedObject 该怎么选?

原问题:假设我们有一个纯 SwiftUI 的数据流。我有一个 List 视图,它拥有一个 @StateObject var listFetcher 属性,这个属性会请求列表项。点击一个列表项会导航到详情视图 DetailView,这个详情视图有一个 detailFetcherObservableObject 属性,它会请求列表项的详情。那么,以什么样的方式组织 DetailView 更好呢?detailFetcher 属性应该采用哪个属性包装器?

  1. 提供一个 init(itemID: Int) 构造器,然后使用 @StateObject 这种方式。这样最终会要求我们在 bodyonAppear 里执行类似 detailFetcher.itemID = itemID 这样的更新操作。
  2. detailFetcher 传入构造器,比如 init(detailFetcher: ObservableObject) 并且将属性标记为 @ObservedObject 。如果采取这种方式,那么这个 detailFetcher 应当存放在哪里?

答复:通常情况下,我们在视图拥有关联对象时使用 @StateObject,比如对象是在视图被创建的时候创建,且需要跟随视图销毁而销毁。对应的,在视图需要引用其他视图或者别的东西拥有的对象时使用 @ObservedObject。这时视图虽然依赖该对象,但两者的生命周期不绑定。

例如,你可以在应用的主屏幕使用 @StateObject 来初始化 app 的 model,然后用 @ObservedObjectmodel 传给详情界面。建议看一看 “解密 SwiftUI” 以了解更多这方面的内容。

@SceneStorage 的存储时机可以自己控制吗?

原问题:有没有办法控制 @SceneStorage 的存储时机呢?我的应用里这个存储大部分都是在根视图,它们似乎只有在应用进入后台时才会被保存,而不是进入后台之前。

答复:我们没有提供控制 场景存储时机的 API,因为我们的本意就是让它们在后台保存,在应用回到前台或者场景重建时恢复。

@State 和 @StateObject 通常是不是都应该定义成 private ?

是的!将它们标记为 private 可以明确状态是属于该视图及其子孙视图的。

当视图的 identity 发生改变时,采用 @State,@StateObject 和 @ObservedObject 有没有一些关键性的区别?

是的,ObservedObject 不包含任何生命周期的暗示 —— 当你使用这个属性包装器时,你需要自行负责管理对象的生命周期。这个对象可能存在于任何地方 —— 比如来自某个 StateObject 或者 app 里的另一个数据结构。

在根视图中以单个 @StateObject 保持整个应用的状态有哪些坏处?

原问题: 我有一个 class Store<State, Action>: ObservableObject,它持有整个 app 的状态,作为 @StateObject 在整个 App 的生命周期内存在,并且以环境对象的方式传递给所有视图。视图发送 action 给这个 store,并根据 store 的状态来更新自己。这种做法让我可以保持 app 状态的一致性。你能说一说这种做法的坏处吗?

答复: 这种做法会使得应用中的所有视图都依赖单一的 Observable 对象。任何一个 Published 属性变化都会强制引用该环境对象的视图刷新。

追加的问题:那么你的建议是什么呢?假设我们有多个视图都依赖相同的数据,并且需要保持视图之间状态的一致性?

答复:对不起,架构问题是依赖具体上下文的,没有放之四海而皆准的策略。收敛你的模型是一种方法,把状态以绑定的方式传递给子组件也是一种方法。

@State/@Binding 相比 @EnvironmentObject 有什么区别?

原问题:把 State 以绑定的形式传递给层级中的每个视图和以环境对象的方式注入,并在需要用到的子视图中访问它们,这两种方式有什么区别?会不会有一者的 dependency graph 比另一者更糟糕的情况?

答复:环境对象与 State 相比稍有不同,因为它必须是 ObservableObject。我们对环境对象做了优化,只会触发实际读取它的视图的更新。

至于 State,当你以绑定的形式传递给子视图时,改变绑定将会反过来改变状态,从而刷新所有持有该状态的视图及其子视图。

出于性能考虑,我们应该如何取舍 ObservableObject 和 @EnvironmentObject?

原问题:从性能角度考虑,我们应该优先使用显式传递 ObservableObject 到子视图的方式还是使用 EnvironmentObject

答复:这两种方式对于任何给定视图都没有大的区别。不过假如你在某些中间层级的视图中并不需要使用目标对象 ,那么 EnvironmentObject 是一种减少样板代码的绝佳方式,而且可以避免创建偶然的依赖。

可以使用 Core Data 的子上下文作为 @StateObject 吗?

原问题:可以使用 CoreData 的子上下文作为 SwiftUI 视图的 @StateObject 变量吗?或者你会建议我以环境对象的方式传递它们,还是在 SwiftUI 之外保存它们?

答复:没有使用 @StateObject 的必要,而且上下文也不是 ObservableObject。通过 environment 向下传递是一个好选择。

“view environmentObject may be missing as an ancestor of this view? ”这个错误是怎么回事呢?

原问题:在使用 @EnvironmentObject 时,貌似我需要给所有层级的子视图传入 .environmentObject modifier。假如我不这么做,编译器就会抛出 “view environmentObject may be missing as an ancestor of this view” 的错误。但我以为 EnvironmentObject 的作用不就是让数据可以不用写很多代码就能让所有的视图使用吗?这是一个设计上的选择还是我的打开方式有误?

答复:感谢提出这个问题。多数情况下,.environmentObject(…) 是能够顺着视图层级往下传递的,但是有一些情况我们是有意地不传递环境对象。例如,一个 NavigationLink 的目标视图不会得到环境对象,因为这里面存在一个冲突:对象是该由发起链接的地方传入呢?还是目标视图的占位符传入,又或者是导航栈里的前一个视图传入?

SheetPopover 因为 bug 的关系也无法得到环境对象,不过这个问题已经被修复了(iOS 15)。

假如你有发现其他地方环境对象没有被传递的情况,请给我们发送反馈,这对于我们修复这个功能很重要。

我可以直接调用 @StateObject 的构造器吗?

原问题:当需要注入数据给一个详情视图,且要让该视图拥有这个数据的 StateObject 时,我直接使用了 StateObject(wrappedValue:) 构造器,就像这样:

1
2
3
4
5
6
7
8
public struct PlanDetailsView: View {

@StateObject var model: PlanDetailsModel

public init(plan: Plan) {
self._model = StateObject(wrappedValue: PlanDetailsModel(plan: plan))
}
}

这种使用构造器的方式是可以接受的吗?据我所知 StateObject 本应该在视图生命周期开始的时候被初始化,而不是放在 View 值的实例化中。我想要确认我的这种写法有没有迫使对象的存储在 View 重新实例化时被再次分配内存。

答复:是的,构造器的这种用法是可以的。你的理解没错:对象会在视图生命周期开始时被创建并且保持。 StateObject 的包装值是一个自动闭包,它只会在视图生命周期开始被调用一次。这也意味着 SwiftUI 会在 plan 第一次创建时捕获它的值。值得注意的是,假如你的视图 identity 没有变化,但传了一个不同的 plan,SwiftUI 不会注意到这个变化。

@State 变量可以在主线程之外更改吗?

原问题: State 的文档上说 “It is safe to mutate state properties from any thread.” 那为什么在非主线程发布 SwiftUI 会发布 PassthroughSubject 会收到运行时警告呢?

答复: State 是线程安全的,可以在任何线程修改。至于 PassthroughSubject ,我假设你是在 @StateObject, @ObserverdObject, 或者 @EnvironmentObject 上下文使用的 ObservableObjectObservableObject 的确要求其属性在主线程修改。 我建议你查阅我的同事 Curt 和 Jessica 在 Discover Concurrency in SwiftUI session 中对于 Swift 并发的讨论。

追加的问题:我是用 onReceiveCurrentValueSubject 在一个 modifier 里设置一个 State,所以我应该报告一个反馈给你们吗?
答复:我明白了。 目前 onReceive 也是期望你在主线程发布变化。一般来说,在主线程通知或者更改与 UI 交互的代码能帮你避免很多麻烦。

假如一个 @ObservedObject 被传入视图,但视图并未使用它的值,@ObservedObject 变化时视图会刷新吗?

原问题:当一个 ObservedObject 对象被传入一个视图时,SwiftUI 会区分实际使用它的视图(body 里用到)和“中间视图”(只是为了把视图传递给子孙)吗?还是所有的视图都会被更新?

答复:是的,两者是有区别的。如果你没有使用 ObservableObject 的属性包装器 (指 @StateObject, @ObservedObject) ,那么视图不会观察和更新。假如你只是需要在中间视图传递 ObservableObject,可以使用常规属性。反之如果你的视图读取了其中的任何值,则应当使用属性包装器,否则视图可能与数据状态不一致。另外, @EnvironmetObject 可以用来取代人工传递 ObservableObject 的好工具。

为了明确说明,我用下面的代码演示 IntermediaryA 是如何做了不必要的 body 计算,而 IntermediaryB 没有这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Model: ObservableObject {
@Published var flag = false
}

struct ExampleView: View {
@StateObject var model = Model()

var body: some View {
HStack(spacing: 20) {
Button("Toggle") {
model.flag.toggle()
}

IntermediaryA(model: model)

IntermediaryB(model: model)

}
}
}

struct IntermediaryA: View {
@ObservedObject var model: Model

var body: some View {
print("IntermediaryA body computed!")

return Flag(model: model)
}
}

struct IntermediaryB: View {
var model: Model

var body: some View {
print("IntermediaryB body computed!")
return Flag(model: model)
}
}

struct Flag: View {
@ObservedObject var model: Model

var body: some View {
Image(systemName: model.flag ? "flag.fill" : "flag.slash.fill")
.font(.largeTitle)
}
}

好在这正是 EnvironmentObject 发挥作用的场景,它能极大地简化代码。 IntermediaryFlag 不再需要构造器里的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Model: ObservableObject {
@Published var flag = false
}

struct ExampleView: View {
@StateObject var model = Model()

var body: some View {
HStack(spacing: 20) {
Button("Toggle") {
model.flag.toggle()
}

Intermediary()
.environmentObject(model)

}
}
}

struct Intermediary: View {
var body: some View {
print("Intermediary body computed!")

return Flag()
}
}

struct Flag: View {
@EnvironmentObject var model: Model

var body: some View {
Image(systemName: model.flag ? "flag.fill" : "flag.slash.fill")
.font(.largeTitle)
}
}

为什么在 sheet 里我的 @EnvironmentObject 会得到 nil ?

原问题:我遇到过几次环境对象为 nil 的情况,发生在我把它们传给一个 sheet 或者 NavigationLink 的场景。因为复现有点难,并且偶然出现,我通常解决这个问题的办法是重新组织我的代码,避免传递环境对象。你知道为什么会这样吗?我怀疑 environmentObject 的内存在视图层级传递过程中被销毁了。希望你能提供帮助!

答复 (工程师 #1):NavigationLink 是设计上有意不传递 EnvironmentObjects 。因为 environmentObject 的继承关系在这里无法明确。我怀疑这是导致你的问题的原因。为了得到预期的结果,你需要显式地传入 environmentObject

答复 (工程师 #2):你也可以把 environmentObject 应用到 NavigationView 本身,那么能够让环境对象对所有推入(导航栈)的内容可用。

我需要支持 iOS 13,但我又需要 @StateObject,我该怎么做?

原问题:为了支持 iOS 13, 我在模型中用的是 @ObservedObject,但我知道 @StateObject 的行为才是我要的。有没有建议的方法可以实现向后兼容。我原以为 if #available 可能可以工作,但它不能用于属性。

答复:为了支持 OS 13,你需要使用 @ObservedObject,并以某种别的方式保持对象,比如使用静态属性或者在应用委托中持有引用。我不认为在 ObservedObject@StateObject 之间切换会给你的场景带来好处,因为通过可用性检查改变对象的所有者的做法不合适。

@EnvironmentObject 和 @ObservedObject 哪个更好?

原问题:通常情况下,在视图里用 EnvironmentObject 更好还是用传递的 ObservedObject 更好?

答复:两种用法都有用户,这取决于你的代码架构。如果你有一个或者很少的大型 ObservableObjects ,大部分视图层级都需要看到它,我通常建议用 EnvironmentObject,因为 SwiftUI 会帮你照顾到所有实际使用该对象的视图。 当然,用 ObservedObject 也能做到,只是相对笨重。前者也不会因为视图没有实际使用对象而让代码变得凌乱。

也就是说,如果你的模型很大程度上不是基于你的视图层级构建的对象图,那么在视图中使用 ObservedObject 抓取自己需要的那部分可能会更合理。

调试

如何调试 AttributeGraph 崩溃?

原问题:有没有方法可以调试 AttributeGraph 的崩溃?我遇到 AttributeGraph precondition failure: “setting value during update”: 696. 这样的错误,可能是哪里的某个 hosting controller,但我不知道如何追踪。

答复:这个错误说明某个 body 或者 updateUIViewController (or NS…) 里的代码正在修改状态。我们在新的 SDK 里提供了一个新的调试工具,可以帮助你缩小排查的范围。在 body 里,如果你写了 Self._printChanges(), SwiftUI 会把导致视图重绘的属性名称通过日志的形式输出到调试区域。(注意下划线。这不是一个 API,只是暴露给调试用的。)

如何对 SwiftUI 代码做性能调优?

原问题:我要如何对 SwiftUI 代码做性能调优,比如知道如何优化我的视图。Instruments 显示的几乎都是 SwiftUI 库的代码,要看清哪部分渲染开销昂贵很困难…

答复:SwiftUI instrument 可以帮助唤出比较重的 body 方法。同时,限制每个视图 body 被重新计算的次数也很重要。强烈建议你去观看 解密 SwiftUI,深入了解这其中的工作方式。提示: 在 body 里调用新的 Self._printChanges() 工具,可以打印导致视图重新计算的属性。技术上它并不是一个 API —— 它有一个下划线,所以只能用于调试。

布局

为了避免影响布局,把 GeometryReader 放进一个透明的 overlay 这种做法可取吗?

原问题:我看到不少在透明的 overlay 里使用 GeometryReader 的例子,这样做是为了避免影响布局… 这个做法可取吗?看起来并不是这个 API 的原本设计的用法。

答复:通常我们会把这个用法看作是对该 API 的误用。假如你有某些需要这么做的场景,欢迎给我们反馈!

怎么获取另一个视图的尺寸?

原问题:有没有办法从另外一个视图获取 GeometryReader 的尺寸。我想把下面的 “???” 文本的高度换成 “Hello world!” 文本的高度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView: View {
var body: some View {
VStack {
Text("Hello world!")
.background(
GeometryReader { proxy in
Color.clear /// placeholder
let _ = print(proxy.size.height) /// 20.333333333333332
}
)
Text("Height of first text is ???")
}
}
}

答复: 你好! 把 GeometryReader 放在视图的背景里可以确保 GeometryReader 不会超出其所包含的视图尺寸,但是提取它的尺寸需要一点小技巧。你可以像这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ContentView: View {
@State private var height = 100.0

var body: some View {
MyView().background {
GeometryReader { proxy in
Color.clear
.onAppear {
height = proxy.size.height
}
.onChange(of: proxy.size.height) {
height = $0
}
}
}
}
}

然后你就能想往常一样使用 State 属性。

⚠️注意:你必须确保你不会因此导致一个连续的布局循环。如果因为你的布局响应了 height 变化而导致 GeometryReader 再次布局,你将陷入一个无限循环。

我认为只要你注意这个 ⚠️ ,你就可以使用这个技术。我想说的是,尽管这个模式已经常见,我们乐于收到反馈,以便提供 SwiftUI 的体验 —— 在反馈中解释你的场景,这能帮助开发团队理解你要做的事情!

关于这项技术,有几种选项(参考前面的一个问题),我重写了问题的例子,以适应 Apple 给出的答复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct ExampleView: View {
@State private var height = 100.0

var body: some View {

VStack {
Text("Hello world!")
.background(
GeometryReader { proxy in
Color.clear
.onAppear {
height = proxy.size.height
}
.onChange(of: proxy.size.height) {
height = $0
}
})
}

Text("Height of first text is \(height)")
}
}

Dependency Graph 可不可以包含循环?

Graph 循环是不允许的。

把视图容器从 HStack 切换成 VStack 时,要怎么保持视图的等价性呢?

原问题:有没有办法在切换 HStackVStack 的同时让 SwiftUI 知道两个 stack 里的内容是相同的?

答复:和这个问题类似的问题有很多,重复之前的答案:是的!你可以试试 matchedGeometryEffect() API,去年引入的,文档在 这里.

frame() 和 Spacer() 要怎么选?

原问题:在大多数场景下, Spacer 的布局行为可以用 .frame(maxWidth:alignment:)(或者 height) 无缝替换。因为 Spacer 是一个实际的视图,参与视图层级的布局,使用 Spacer 会消耗更多的内存和 CPU 资源 “解密 SwiftUI” 也提到 “modifier is cheap”。所以我是不是应该尽可能地用 .frame 代替 Spacer

答复 (工程师 #1):尽管 Spacer 是一个视图,但它最终并不显示任何东西,所以其本身非常轻量。 使用 .frame 会引入其他行为导致视图改变尺寸。两者都有使用场景,所以请在各自合适的场景下选用。

答复 (工程师 #2):稍微补充一点,尽管你可以用两种方式取得几乎一致的效果,但因为性能的差异微乎其微,我会强烈建议在决策时把代码可读性放在性能/内存占用之前。假如 Spacer 使你的布局意图更清晰,那么就用它,反之亦然。

为了演示 frame 和 Spacer 表现不同的场景,请看下面的代码。
因为在 HStack 容器的间距参数在未指定时, SwiftUI 会采用自动留白,这时 Spacer 和 .frame 的处理方式就是不一样的。虽然两个 HStack 视图的输出看起非常相似,但并不是相等的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct ExampleView: View {
var body: some View {
VStack {
HStack {
Spacer()

Circle().frame(width: 50, height: 50)

Spacer()

Circle().frame(width: 50, height: 50)
}
.border(Color.blue)

HStack {
Color.clear.frame(maxWidth: .infinity, maxHeight: 0)

Circle().frame(width: 50, height: 50)

Color.clear.frame(maxWidth: .infinity, maxHeight: 0)

Circle().frame(width: 50, height: 50)
}
.border(Color.blue)
}
}
}

不过通过指定 HStack 的间距可以让两个 HStack 生成相同的结果。

“Bound preference SizePreferenceKey tried to update multiple times per frame” 这是什么错误?

原问题: 如何避免出现 Bound preference SizePreferenceKey tried to update multiple times per frame? 这个错误?

看起来你遇到循环更新的问题。举个例子,一个 GeometryReader 对一个属性写入,会导致包含它的视图重绘,这又会导致 GeometryReader 再次写入属性。应当避免创建这类循环。通常我们可以通过把 GeometryReader 移到更外层的视图,以便其尺寸不发生变化来解决这个问题。不过没有看到你的代码,我恐怕不能再给出更具体的建议,希望对你解决这个问题有帮助。

参考

  1. Random Lessons from the SwiftUI Digital Lounge
  2. 解密 SwiftUI